﻿//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: OPathParser_1.cs
//
//  Description: Parses OPath to an ObjectExpression.
// 
//--------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using Chili.Opf3.Relations;
using Chili.Opf3.Mapping;

namespace Chili.Opf3.Query.OPathParser
{
	/// <summary>
	/// This class is the internal OPath parser of the framework. It is used by the
	/// <see cref="Opf3.Query.OPath">OPath</see> class to parse OPath expressions.
	/// </summary>
	internal sealed class OPathParser<T>
	{
		private int _openBrackets = 0;
		private ObjectQuery<T> _query = null;

		/// <summary>
		/// Parses the <see cref="Opf3.Query.ObjectQuery{T}">ObjectQuery</see> and
		/// returns an <see cref="Opf3.Query.ObjectExpression">ObjectExpression</see> containing
		/// the parsed query.
		/// </summary>
		/// <param name="query"><see cref="Opf3.Query.ObjectQuery{T}">ObjectQuery</see> that is parsed.</param>
		/// <returns>An <see cref="Opf3.Query.ObjectExpression">ObjectExpression</see> that contains the
		/// parsed <see cref="Opf3.Query.ObjectQuery{T}">ObjectQuery</see>.</returns>
		/// <exception cref="ArgumentNullException">Query is null.</exception>
		public ObjectExpression Parse(ObjectQuery<T> query)
		{
			if (query == null)
				throw new ArgumentNullException("query");

			// Set the query as current parsed query.
			_query = query;

			// Create the ObjectExpression.
			TypeMapping mapping = TypeMapping.GetTypeMapping(typeof(T));
			ObjectExpression objectExpression = new ObjectExpression(QueryTypes.Load, mapping.PersistentType, mapping.PersistentAttribute);
			objectExpression.Timeout = query.Timeout;

			// Check if we have to parse the string. If there is no string we leave with an empty ObjectExpression.
			if (query.Value == null || query.Value.Length == 0)
				return objectExpression;

			// Create the tokenizer.
			Tokenizer tokenizer = new Tokenizer(query.Value);
			// Parse the query.
			objectExpression.Items.Add(InternalParse(tokenizer, new OEWhereCondition(), query.Parameters, typeof(T),
				objectExpression));

			// If a bracket is still open throw an exception.
			if (_openBrackets > 0)
				throw new OPathException("Missing closing bracket.", string.Empty, query.Value);
			return objectExpression;
		}

		/// <summary>
		/// Method is called to parsed a given item of the 
		/// <see cref="Opf3.Query.ObjectExpression">ObjectExpression</see>. This routine
		/// is called each time an item is parsed.
		/// </summary>
		/// <param name="tokenizer">The tokenizer class returning the tokens that are parsed.</param>
		/// <param name="item">The current item that is parsed.</param>
		/// <param name="parameters">A list of parameters of the query.</param>
		/// <param name="type">The type of the current persistent object that is parsed. This type changes
		/// on joines.</param>
		/// <param name="objectExpression">The ObjectExpression that is compiled.</param>
		/// <returns>An item that contains other parsed items (contains a three).</returns>
		private OEItem InternalParse(Tokenizer tokenizer, OEItem item, IList<object> parameters, Type type,
			ObjectExpression objectExpression)
		{
			foreach (Token token in tokenizer)
			{
				switch (token.TokenKind)
				{
					case TokenKinds.Concat:
						if (item.Items.Count == 0)
						{
							throw new OPathException("Concat can't be first item in an OPath expression.",
								token.Value, _query.Value);
						}
						item.Items.Add(new OEConcat(token.Value));
						break;

					case TokenKinds.OpenParenthesis:
						_openBrackets++;
						item.Items.Add(InternalParse(tokenizer, new OEBrackets(), parameters, type, objectExpression));
						break;

					case TokenKinds.CloseParenthesis:
						if (_openBrackets == 0)
						{
							throw new OPathException("Can't close a bracket without opening one.",
								token.Value, _query.Value);
						}
						_openBrackets--;
						return item;

					case TokenKinds.Command:
					case TokenKinds.Member:
						item.Items.Add(ParseCondition(tokenizer, token, parameters, type));
						break;

					case TokenKinds.Join:
						item.Items.Add(ParseJoin(token, parameters, type, objectExpression));
						break;

					case TokenKinds.SortBy:
						// HACK: This should be moved outside. It has nothing to do here! Will be removed
						// when OPath will be improved the next time!
						objectExpression.Items.Add(ParseSortBy(token, type));
						break;

					default:
						throw new OPathException("Invalid item encountered in the OPath expression.", token.Value,
							_query.Value);
				}
			}
			return item;
		}

		/// <summary>
		/// Parses a join condition.
		/// </summary>
		/// <param name="token">The current token that is parsed.</param>
		/// <param name="parameters">A list of parameters from the query.</param>
		/// <param name="type">Type of the persistent object that is parsed.</param>
		/// <returns>An item that contains other parsed items (contains a three).</returns>
		/// <param name="objectExpression">The ObjectExpression that is compiled.</param>
		private OEJoin ParseJoin(Token token, IList<object> parameters, Type type, ObjectExpression objectExpression)
		{
			string value = token.Value;
			int firstBracket = value.IndexOf("[");
			// Get the name of the persistent that is joined.
			string persistentName = value.Substring(0, firstBracket);

			TypeMapping currentTypeMapping = TypeMapping.GetTypeMapping(type);
			Type joinedType = null;

			// HACK: Get the first that is matching.
			foreach (RelationInfo info in currentTypeMapping.Relations)
			{
				Type persistentType = info.ChildTypeMapping.PersistentType;

				if (persistentType.FullName.Contains(persistentName))
				{
					joinedType = persistentType;
					break;
				}
			}

			if (joinedType == null)
			{
				throw new OPathException("Could not resolve type of the join condition.", token.Value,
					_query.Value);
			}

			// Get the mapping for the joined type.
			TypeMapping joinedTypeMapping = TypeMapping.GetTypeMapping(joinedType);
			OEJoin join = new OEJoin(joinedTypeMapping.PersistentAttribute);

			// Create a new tokenizer that analyzes the join.
			Tokenizer tokenizer = new Tokenizer(value.Substring(firstBracket + 1, value.Length - firstBracket - 2));
			// Parse the conditions in the join set by the user.
			InternalParse(tokenizer, join, parameters, joinedType, objectExpression);

			// Add a concat to the join conditions and open a braket.
			OEConcat concat = new OEConcat("And");
			join.Items.Add(concat);
			OEBrackets bracket = new OEBrackets();
			join.Items.Add(bracket);

			// Loop over all relation mappings and add the values to the external condition.
			foreach (RelationInfo info in currentTypeMapping.Relations.GetRelationMapping(joinedType))
			{
				if (bracket.Items.Count != 0)
					bracket.Items.Add(new OEConcat("Or"));

				// Create an inner brackets object.
				OEBrackets innerBrackets = new OEBrackets();
				// Loop over all related properties pairs.
				foreach (RelatedPropertiesPair pair in info.RelatedProperties)
				{
					if (innerBrackets.Items.Count > 0)
						innerBrackets.Items.Add(new OEConcat("And"));

					OECondition condition = new OECondition();
					condition.Items.Add(new OEMember(pair.ParentMember.Name, pair.ParentMember.FieldAttribute,
						info.ParentTypeMapping.PersistentAttribute));
					condition.Items.Add(new OEComparer("="));

					if (info.WeakTypeMapping == null)
					{
						condition.Items.Add(new OEMember(pair.ChildMember.Name, 
							pair.ChildMember.FieldAttribute, info.ChildTypeMapping.PersistentAttribute));
					}
					else
					{
						// There is a weak persistent object. The connection has to be made with the weak object.
						condition.Items.Add(new OEMember(pair.ChildMember.Name, 
							pair.ChildMember.FieldAttribute, info.WeakTypeMapping.PersistentAttribute));
					}
					// Add the condition.
					innerBrackets.Items.Add(condition);
				}

				if (info.WeakTypeMapping != null)
				{
					// Since there is a weak persistent we have to make the connection from the weak object
					// to the table with the data we are interested in.
					foreach (RelatedPropertiesPair pair in info.WeakRelatedProperties)
					{
						innerBrackets.Items.Add(new OEConcat("And"));

						OECondition condition = new OECondition();
						condition.Items.Add(new OEMember(pair.ParentMember.Name,
							pair.ParentMember.FieldAttribute, info.WeakTypeMapping.PersistentAttribute));
						condition.Items.Add(new OEComparer("="));
						condition.Items.Add(new OEMember(pair.ChildMember.Name,
							pair.ChildMember.FieldAttribute, info.ChildTypeMapping.PersistentAttribute));
						// Add the condition.
						innerBrackets.Items.Add(condition);
					}

					// Add the persistent attribute of the weak persistent class.
					objectExpression.PersistentAttributes.Add(info.WeakTypeMapping.PersistentAttribute);
				}
				// Add the condition to the join.
				bracket.Items.Add(innerBrackets);
			}
			return join;
		}

		/// <summary>
		/// Parses a command.
		/// </summary>
		/// <param name="token">The token that contains the command.</param>
		/// <param name="parameters">A list of parameters from the query.</param>
		/// <param name="type">Type of the persistent object that is parsed.</param>
		/// <returns>An item that contains other parsed items (contains a three).</returns>
		private OECommand ParseCommand(Token token, IList<object> parameters, Type type)
		{
			string value = token.Value;
			int firstBracket = value.IndexOf("(");
			// Get the name of the command.
			string commandName = value.Substring(0, firstBracket);

			// Create a new command object.
			OECommand command = new OECommand(commandName);

			// Create a new tokenizer that analyzes the command.
			Tokenizer tokenizer = new Tokenizer(value.Substring(firstBracket + 1, value.Length - firstBracket - 2));

			foreach (Token innerToken in tokenizer)
			{
				switch (innerToken.TokenKind)
				{
					case TokenKinds.Command:
						command.Items.Add(ParseCommand(innerToken, parameters, type));
						break;

					case TokenKinds.Member:
						command.Items.Add(ParseMember(innerToken, innerToken.Value, type));
						break;

					case TokenKinds.Parameter:
						command.Items.Add(ParseParameter(innerToken, parameters));
						break;

					case TokenKinds.Unknown:
						// If the token is unknown it is possibly a argument of the command.
						if (innerToken.Value != ",")
							command.Arguments.Add(innerToken.Value);
						break;

					default:
						throw new OPathException("Invalid item encountered in command.", innerToken.Value,
							_query.Value);
				}
			}
			return command;
		}

		/// <summary>
		/// Parses a member.
		/// </summary>
		/// <param name="memberName">Name of the member.</param>
		/// <param name="token">Token that contains the property.</param>
		/// <param name="type">Type of the persistent object that is parsed.</param>
		/// <returns>A property object that contains the parsed property</returns>
		private OEMember ParseMember(Token token, string memberName, Type type)
		{
			TypeMapping typeMapping = TypeMapping.GetTypeMapping(type);

			IMemberInfo info = typeMapping.Members.GetMemberInfoByName(memberName);
			if (info == null)
			{
				throw new OPathException(string.Format("Cannot convert member '{0}.{1}' to field. Member is not found in object.", 
					type.FullName, memberName), token.Value, _query.Value);
			}

			return new OEMember(memberName, info.FieldAttribute, typeMapping.PersistentAttribute);
		}

		/// <summary>
		/// Parses a parameter. The parameter could be an inline parameter or a parameter that
		/// is referenced inside the OPath by a Tag ({0}, {1}, ...).
		/// </summary>
		/// <param name="token">Token that contains the parameter.</param>
		/// <param name="parameters">A list of parameters from the query.</param>
		/// <returns>An item that contains the parameter.</returns>
		private OEItem ParseParameter(Token token, IList<object> parameters)
		{
			if (token.Value.Contains("{"))
			{
				// Get the index and return the parameter at that index.
				int index = int.Parse(token.Value.Replace("{", "").Replace("}", ""));
				try
				{
					// Create a new parameter.
					if (parameters[index] == null)
						return new OEParameter(null, typeof(object));
					else
						return new OEParameter(parameters[index], parameters[index].GetType());
				}
				catch (Exception ex)
				{
					throw new OPathException(string.Format("Parameter with index {0} is missing.", index), token.Value, _query.Value, ex);
				}
			}
			else
			{
				return new OEInlineParameter(token.Value);
			}
		}

		/// <summary>
		/// Parses a condition.
		/// </summary>
		/// <param name="tokenizer">The tokenizer that is used to parse the condition.</param>
		/// <param name="token">The token containing the condition.</param>
		/// <param name="parameters">A list of parameters from the query.</param>
		/// <param name="type">The type of the persistent object that is parsed.</param>
		/// <returns>A parsed condition or a three of items (if the condition contains commands).</returns>
		private OECondition ParseCondition(Tokenizer tokenizer, Token token, IList<object> parameters, Type type)
		{
			OECondition condition = new OECondition();
			Token current = token;

			for (int i = 0; i < 3; i++)
			{
				switch (current.TokenKind)
				{
					case TokenKinds.Member:
						condition.Items.Add(ParseMember(current, current.Value, type));
						break;

					case TokenKinds.Command:
						condition.Items.Add(ParseCommand(current, parameters, type));
						break;

					case TokenKinds.Comparer:
						if (current.Value.ToLower() == "isnot")
							condition.Items.Add(new OEComparer("is not"));
						else
							condition.Items.Add(new OEComparer(current.Value));
						break;

					case TokenKinds.Parameter:
						condition.Items.Add(ParseParameter(current, parameters));
						break;

					default:
						throw new OPathException("A condition can only contain a command or a property.",
							current.Value, _query.Value);
				}

				// Do not advance in the last run.
				if (i != 2)
				{
					if (!tokenizer.MoveNext())
					{
						// We set here as token an empty string, since we go over the end of the OPath expression
						// and the condition isn't terminated.
						throw new OPathException("A unterminated condition has been encountered.", string.Empty,
							_query.Value);
					}
					current = tokenizer.Current;
				}
			}

			return condition;
		}

		/// <summary>
		/// Parses a SortBy condition.
		/// </summary>
		/// <param name="token">The token that contains the SortBy condition.</param>
		/// <param name="type">The type of the persistent object that is parsed.</param>
		/// <returns>A parsed SortBy condition.</returns>
		private OESortBy ParseSortBy(Token token, Type type)
		{
			OESortBy sortBy = new OESortBy();

			string[] sortExpressions = token.Value.Split(',');

			for (int j = 0; j < sortExpressions.Length; j++)
			{
				string[] tokens = sortExpressions[j].Split(' ');
				SortOrders sortOrder = SortOrders.Ascending;
				OEMember member = null;

				for (int i = 0; i < tokens.Length; i++)
				{
					if (tokens[i].Length == 0)
						continue;

					string tok = tokens[i].ToLower();

					switch (tok)
					{
						case "sortby":
							break;
						case "asc":
							sortOrder = SortOrders.Ascending;
							break;
						case "desc":
							sortOrder = SortOrders.Descending;
							break;
						default:
							member = ParseMember(token, tokens[i], type);
							break;
					}
				}

				if (member == null)
					throw new OPathException("SortBy without property found.", token.Value, _query.Value);

				// Create a corresponding SortByMember instance representing the current sort expression.
				OESortByMember sortByMember = new OESortByMember(sortOrder);
				// Add the property as SortByMember subitem.
				sortByMember.Items.Add(member);

				// Add the SortByMember as SortBy subitem.
				sortBy.Items.Add(sortByMember);
			}

			return sortBy;
		}
	}
}
